
import time
import socket
import restutils
import urllib
import xml.dom.minidom
import nirvanixutils
import os.path
import sys
from urlparse import urlparse
from threading import Thread, Lock
import httplib
import hashlib
import base64
# If using python 3.0+ change to import queue
import Queue

#################################################
# See the settings file for configurable values.#
#################################################
import settings

activethreads = 0
ex_queue = None

# This class is used to create the idea of a static method by allowing direct class method calls.
class Session:
	def __init__(self, appKey, username, password):
		self.sessionToken = None
		auth = Nirvanix('Authentication', None)
		self.options = {'appKey':appKey,'username':username,'password':password}
		doc = auth.Login(self.options,True)
		self.sessionToken = doc.getElementsByTagName("SessionToken")[0].childNodes[0].data
		if settings.debug:
			print "DEBUG: Session() - Session Token=" + self.sessionToken
	def __del__(self):
		if self.sessionToken:
			auth = Nirvanix('Authentication', None)
			logoutOptions = {'sessionToken':self.sessionToken}
			auth.Logout(logoutOptions,True)
			if settings.debug:
				print "DEBUG: Session.__del__ - session destroyed"
	def updateSession(self):
		auth = Nirvanix('Authentication', None)
		# reuse the options set previously during init.
		doc = auth.Login(self.options,True)
		self.sessionToken = doc.getElementsByTagName("SessionToken")[0].childNodes[0].data
		if settings.debug:
			print "DEBUG: Session() - Session Token=" + self.sessionToken
		
secure_services_url = "https://services.nirvanix.com"
services_url = "http://services.nirvanix.com"

class Nirvanix(object):
	def __init__(self,namespace, session):
		self.namespace = namespace
		self.session = session

	def genericFunctionHandler(self,name):
		def handlerFunction(*args,**kwargs):
			# Determine if the connection should be secure
			if args[1]:
				connection = restutils.Connection(secure_services_url)
			else:
				connection = restutils.Connection(services_url)
            
			# Build parameters based on the passed in associative array
			if (self.session):
				params = 'sessionToken=' + self.session.sessionToken + '&'
			else:
				params = ''
				
			for key in args[0]:
				if isinstance(args[0][key], list):
					for value in args[0][key]:
						params += key + '=' + urllib.quote(value) + '&'
				else:
					params += key + '=' + urllib.quote(unicode(args[0][key])) + '&'
			if settings.debug:
				print "DEBUG: Parameters being sent: " + params

			# build the URL of the method to access.
			method_name = '/ws/' + self.namespace + '/' + name + '.ashx'

			if settings.debug:
				print "DEBUG: Nirvanix.geneircFunctionHandler.handlerFunction calling method: " + method_name

			# Do the request and get the xml back.
			response_xml = connection.request_post(method_name, params)
			try:
				return nirvanixutils.check_response(response_xml)
			except nirvanixutils.NirvanixException, e:
				# session expired
				if e.code == 80006:
					self.session.updateSession()
					return eval("self." + name + "(*args)")
				else:
					raise e
							
		return handlerFunction

	def __getattr__(self,name):
		return self.genericFunctionHandler(name)

class NirvanixDownloader(object):
	def __init__(self, session, show_percentage = True, number_threads = 1, verify_file = True):
		self.session = session
		self.verify_file = verify_file
		self.number_threads = number_threads
		self.show_percentage = show_percentage
		self.file_size = 0
		if settings.debug:
			print "DEBUG: NirvanixDownloader created"

	def gethead(self, hostname, path, redirects=0):
		global ex_queue
		retryCount = 0
		strerror = ""
		while retryCount < settings.number_of_retries:
			try:
				proxy = os.getenv('http_proxy')
				if proxy == None:
					conn = httplib.HTTPConnection(hostname)
					conn.request('HEAD',path,None,{})
				else:
					proxy_url = urlparse(proxy)
					conn = httplib.HTTPConnection(proxy_url.netloc)
					conn.request('HEAD',"http://" + hostname + path,None,{})

				resp = conn.getresponse()
				# If this is the case the file is likely offline so we should exclude that node.
				if resp.status in (301,302):
					if redirects > settings.max_redirects:
						raise nirvanixutils.NirvanixException("Exceeded maximum number of redirects.", -1)
					ex_queue.put(hostname.split('.')[0])
					return
				# either sets the redirected name or the original.
				self.hostname = hostname
				self.file_size = int(resp.getheader('Content-Length'))
				self.md5 = str(resp.getheader('Content-MD5')).strip()
				if self.md5 == None:
					raise nirvanixutils.NirvanixException("Invalid download URL MD5 was not provided.", -1)
				if settings.debug:
					print "DEBUG: Retrieved HEAD, MD5 and filesize."
			except (socket.error, socket.timeout), strerror:
				if settings.debug:
					print "DEBUG: Socket error connecting to node, retrying. Error: %s" % (strerror)
				retryCount+=1
				time.sleep(settings.retry_sleep_time*retryCount)
				continue
			except httplib.BadStatusLine, e:
				# handle bad status and exit retry logic.
				if settings.debug:
					print "DEBUG: ", e, e.line
				retryCount+=1
				time.sleep(settings.retry_sleep_time*retryCount)
				continue
			except httplib.HTTPException:
				# error downloading file this is likey a failed read or timeout so retry.
				print "DEBUG: Http error connecting to node, retrying. Error: %s" % (strerror)
				retryCount+=1
				time.sleep(settings.retry_sleep_time*retryCount)
				continue
			else:
				# Exit from the error handling logic.
				break
		# we made it here with an error.  Start over building the excludedNodes
		if retryCount > 0:
			if settings.debug:
				print "DEBUG: Node unavailable, re-routing to a new node."
			ex_queue.put(hostname.split('.')[0])


	def downloadfile(self, localFilePath, sourcePath, excludedNodes = []):
		global activethreads, ex_queue
		# used to communicate retry failures back to the main thread.
		ex_queue = Queue.Queue()
		activethreads = 0
		threadstotal = 0
		while 1:
	        # Get download nodes
			imfs = Nirvanix('IMFS', self.session)
			options = {'filePath':sourcePath,'expiration':'86400','iprestricted':'false'}
			if len(excludedNodes) > 0:
				nodes = []
				for excludedNode in excludedNodes:
					nodes.append(excludedNode)
				options.update({'excludedNode':nodes})
			
			if settings.debug:
				print "DEBUG: NirvanixDownloader.downloadfile called for " + sourcePath
	
			# Get the node where the file should be downloaded from.
			doc = imfs.GetOptimalUrls(options,False)

			downloadUrl = doc.getElementsByTagName("DownloadURL")[0].childNodes[0].data
			if settings.debug:
				print "DEBUG: NirvanixDownloader download node: " + downloadUrl
			
			url = urlparse(downloadUrl)
			scheme, hostname, path, q, query, position=url
			self.gethead(hostname, path)
			if not ex_queue.empty():
				excludedNodes.append(ex_queue.get())
				return self.downloadfile(localFilePath, sourcePath, excludedNodes)
			f = open(localFilePath,'wb+')
			start = time.time()
			totalparts = len(range(0, self.file_size, settings.download_buffer_size))
			for start_pos in range(0, self.file_size, settings.download_buffer_size):
				if start_pos + settings.download_buffer_size < self.file_size:
					end_pos = start_pos + settings.download_buffer_size
				else:
					end_pos = self.file_size
				activethreads = activethreads + 1
				threadstotal = threadstotal + 1
				restutils.DownloadThread(self.hostname, path, f, self.file_size, start_pos, end_pos).start()
				while (activethreads >= self.number_threads):
					if ex_queue.empty():
						time.sleep(1)
					else:
						excludedNodes.append(ex_queue.get())
						if settings.debug:
							print "DEBUG: Waiting for active threads to finish."
						while activethreads > 0:
							time.sleep(1)
						return self.downloadfile(localFilePath, sourcePath, excludedNodes)
				if self.show_percentage:
					percent = (float(threadstotal - activethreads)/float(totalparts))
					count = int(percent * 20)
					if threadstotal - activethreads <= 0:
						kbpersec = 0
					else:
						kbpersec = float(((threadstotal - activethreads) * settings.download_buffer_size) / 1024) // float(time.time() - start)
					sys.stdout.write("\r%-30s |%-20s|%3d%% %6dkb/sec" % (localFilePath,'#'*count,percent*100, kbpersec))
					sys.stdout.flush()					
			
			prevthreads = activethreads			
			while activethreads > 0:
				if prevthreads > activethreads:
					if self.show_percentage:
						percent = (float(threadstotal - activethreads)/float(totalparts))
						count = int(percent * 20)

						if threadstotal - activethreads <= 0:
							kbpersec = 0
						else:
							kbpersec = float(((threadstotal - activethreads) * settings.download_buffer_size) / 1024) // float(time.time() - start)
						sys.stdout.write("\r%-30s |%-20s|%3d%% %6dkb/sec" % (localFilePath,'#'*count,percent*100, kbpersec))
						sys.stdout.flush()
					prevthreads = activethreads
				time.sleep(1)
			end = time.time()
			f.close()
			if self.show_percentage:
				kbpersec = float(self.file_size / 1024) // float(end - start)
				sys.stdout.write("\r%-30s |%-20s|%3d%% %6dkb/sec" % (localFilePath,'#'*20,100, kbpersec))
				sys.stdout.flush()

			if self.verify_file:
				if settings.debug:
					print "DEBUG: Verifying MD5..."
				f = open(localFilePath, 'rb+')
				md5 = hashlib.md5()
				md5.update(f.read())
				md5_file = base64.encodestring(md5.digest()).strip()
				if md5_file != self.md5:
					raise nirvanixutils.NirvanixException("MD5 Missmatch, download failed. '%s' != '%s'" % (md5_file, self.md5), -1) 
				f.close()
				if settings.debug:
					print "DEBUG: MD5 Matched expected value. File MD5: %s Nirvanix MD5: %s" % (md5_file, self.md5)
			return


class NirvanixUploader(object):
	def __init__(self, session, show_percentage = True):
		self.session = session
		self.show_percentage = show_percentage
		if settings.debug:
			print "DEBUG: NirvanixUploader created"
		
	def uploadfile(self, localFilePath, destFolderPath, destFilename = None, excludedNodes = [], callbackURL = None, overwrite = False, iprestricted = True):
		if destFilename == None:
			destFilename = localFilePath

		if settings.debug:
			print "DEBUG: NirvanixUploader.uploadfile started for local file: " + localFilePath

        # Get Storage Node
		imfs = Nirvanix('IMFS', self.session)
		filesize = os.path.getsize(localFilePath)
		options = {'sizebytes':str(filesize),'destFolderPath':destFolderPath, 'iprestricted':iprestricted, 'fileoverwrite':str(overwrite)}
		while 1:
			if len(excludedNodes) > 0:
				nodes = []
				for excludedNode in excludedNodes:
					nodes.append(excludedNode)
				options.update({'excludedNode':nodes})
			# Get the storage node and token where the file should be uploaded to.
			doc = imfs.GetStorageNodeExtended(options,False)
			uploadToken = doc.getElementsByTagName("UploadToken")[0].childNodes[0].data
			uploadHost = doc.getElementsByTagName("UploadHost")[0].childNodes[0].data

			if settings.debug:
				print "DEBUG: NirvanixUploader.uploadFile upload host: " + uploadHost

			# TODO: Update services_url with uploadHost when uploader is merged with rest poster
			connection = restutils.Connection(services_url, self.show_percentage)
			try:
				return connection.uploadfile(uploadHost, uploadToken, localFilePath, 
					destFolderPath, destFilename, callbackURL)
			except (restutils.RetryException):
				excludedNodes.append(uploadHost.split('.')[0])

